3.0 Commonjs,CMD,AMD

Commonjs规范

CommonJS定义的模块分为:{模块引用(require)} {模块定义(exports)} {模块标识(module)};
require()用来引入外部模块;exports对象用于导出当前模块的方法或变量,唯一的导出口;module对象就代表模块本身。 commonjs是服务器模块的规范,nodejs实现了规范

  • 单独的文件就是一个模块,每个模块拥有自己的作用域;

  • 通过module.exports来将对象和方法抛出去;

  • 通过require来引入,就可以访问挂载的对象,并且直接调用变量;

每个模块

  • 每个都有个module对象,这个对象不是全局变量,而是本地变量
  • exports = module.exports,可以放弃使用exports,只使用module.exports

CommonJS就是个同步加载方案,因为服务器一般文件都在本地硬盘,

require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。

CommonJS模块的特点如下:

所有代码都运行在模块作用域,不会污染全局作用域。
模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,
以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
模块加载的顺序,按照其在代码中出现的顺序。

require的内部处理流程

require命令是CommonJS规范之中,用来加载其他模块的命令。

它其实不是一个全局命令,而是指向当前模块的module.require命令,而后者又调用Node的内部命令Module._load。

Module._load = function(request, parent, isMain) {
  // 1. 检查 Module._cache,是否缓存之中有指定模块
  // 2. 如果缓存之中没有,就创建一个新的Module实例
  // 3. 将它保存到缓存
  // 4. 使用 module.load() 加载指定的模块文件,
  //    读取文件内容之后,使用 module.compile() 执行文件代码
  // 5. 如果加载/解析过程报错,就从缓存删除该模块
  // 6. 返回该模块的 module.exports
};
上面的第4步,采用module.compile()执行指定模块的脚本,逻辑如下。

Module.prototype._compile = function(content, filename) {
  // 1. 生成一个require函数,指向module.require
  // 2. 加载其他辅助方法到require
  // 3. 将文件内容放到一个函数之中,该函数可调用 require
  // 4. 执行该函数
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

上面的第1步和第2步,require函数及其辅助方法主要如下

require函数及其辅助方法主要如下。

require(): 加载外部模块
require.resolve():将模块名解析到一个绝对路径
require.main:指向主模块
require.cache:指向所有缓存的模块
require.extensions:根据文件的后缀名,调用不同的执行函数
1
2
3
4
5

++++++++++++++++++++++++++++++++++++++++++++++++++++

AMD

意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行
代表:require.js

推崇依赖前置,提前执行

require.js的诞生,就是为了解决这两个问题:

(1)实现js文件的异步加载,避免网页失去响应;

(2)管理模块之间的依赖性,便于代码的编写和维护。

+++++++++++++++++++++++++++++++++++++++++++++++++++

CMD

推崇依赖就近原则,延迟执行
代表: sea.js

sea.js 与 RequireJS 异同:

模块加载器,倡导模块化开发理念

+++++++++++++++++++++++++++++++++++++++++++

ES6

CommonJS 与 ES6 有两个重大差异

  • 1.CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • 2.CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。
而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

ES6 模块的运行机制与 CommonJS 不一样。
JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用
等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值

换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变
因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块

import 和 require

  • 运行时加载: CommonJS 模块就是对象;

    • 即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。
    即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

  • import 命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。
    也正因为这个,使得静态分析成为可能

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。
而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

想起来 JavaScript 的词法作用域和动态作用域

词法作用域 又称 静态作用域

作用域是指程序源代码中定义变量的区域。

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。

与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

猜猜两段代码各自的执行结果是多少?

两段代码都会打印:local scope。

原因也很简单,因为JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置。

两个函数 执行上下文栈 不一样:

当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。

而引用《JavaScript权威指南》的回答就是:

JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。
嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,
不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。

参考